﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Pfz.Threading;
using System.Runtime.InteropServices;

namespace Pfz.Caching
{
	/// <summary>
	/// This is a dictionary that allow values to be collected.
	/// </summary>
	[Serializable]
	public sealed class WeakDictionary<TKey, TValue>:
		ReaderWriterSafeDisposable,
		IDictionary<TKey, TValue>,
		ISerializable,
		IGarbageCollectionAware
	where
		TValue: class
	{
		#region Static Area
			private static Func<TValue> _valueConstructor = _GetDefaultConstructorDelegate();
			
			private static Func<TValue> _GetDefaultConstructorDelegate()
			{
				if (typeof(TValue).GetConstructor(Type.EmptyTypes) == null)
					return null;
					
				return ReflectionHelper.GetDefaultConstructorDelegate<TValue>(typeof(TValue));
			}
		#endregion

		#region Private dictionary of KeepAliveGCHandle
			private Dictionary<TKey, GCHandle> _dictionary = new Dictionary<TKey, GCHandle>();
		#endregion
		
		#region Constructor
			/// <summary>
			/// Creates the dictionary.
			/// </summary>
			public WeakDictionary()
			{
				GCUtils.RegisterForCollectedNotification(this);
			}
		#endregion
		#region Dispose
			/// <summary>
			/// Frees all handles used to know if an item was collected or not.
			/// </summary>
			protected override void Dispose(bool disposing)
			{
				if (disposing)
					GCUtils.UnregisterFromCollectedNotification(this);
				
				var dictionary = _dictionary;
				if (dictionary != null)
				{
					_dictionary = null;
					
					foreach(GCHandle handle in dictionary.Values)
						handle.Free();
				}
				
				base.Dispose(disposing);
			}
		#endregion
		#region _Collected
			private int _collectionCount;
			void IGarbageCollectionAware.OnCollected()
			{
				try
				{
					using(DisposeLock.WriteLock())
					{
						if (WasDisposed)
						{
							GCUtils.UnregisterFromCollectedNotification(this);
							return;
						}
							
						var oldDictionary = _dictionary;
						var newDictionary = new Dictionary<TKey, GCHandle>(oldDictionary.Count);
						foreach(var pair in oldDictionary)
						{
							var handle = pair.Value;
							
							if (handle.Target != null)
								newDictionary.Add(pair.Key, pair.Value);
							else
								handle.Free();
						}
						
						_dictionary = newDictionary;
						_collectionCount++;
					}
				}
				catch
				{
				}
			}
		#endregion
		
		#region Properties
			#region Count
				/// <summary>
				/// Gets the number of items in this dictionary.
				/// </summary>
				public int Count
				{
					get
					{
						using(DisposeLock.ReadLock())
						{
							CheckUndisposed();
							
							return _dictionary.Count;
						}
					}
				}
			#endregion
			#region this[]
				/// <summary>
				/// Gets or sets a value for the specified key.
				/// Returns null if the item does not exist. The indexer, when
				/// used as an IDictionary throws an exception when the item does
				/// not exist.
				/// </summary>
				public TValue this[TKey key]
				{
					get
					{
						if (key == null)
							throw new ArgumentNullException("key");
					
						using(DisposeLock.ReadLock())
						{
							CheckUndisposed();
							
							GCHandle handle;
							if (_dictionary.TryGetValue(key, out handle))
								return (TValue)handle.Target;
						}

						return default(TValue);
					}
					set
					{
						if (key == null)
							throw new ArgumentNullException("key");
					
						using(DisposeLock.WriteLock())
						{
							CheckUndisposed();
							
							if (value == null)
								_Remove(key);
							else
							{
								GCHandle handle;
								
								var dictionary = _dictionary;
								if (dictionary.TryGetValue(key, out handle))
									handle.Target = value;
								else
									_Add(key, value, dictionary);
							}
						}
					}
				}
			#endregion
			
			#region Keys
				/// <summary>
				/// Gets the Keys that exist in this dictionary.
				/// </summary>
				public ICollection<TKey> Keys
				{
					get
					{
						using(DisposeLock.ReadLock())
						{
							CheckUndisposed();
							
							return _dictionary.Keys.ToArray();
						}
					}
				}
			#endregion
			#region Values
				/// <summary>
				/// Gets a copy of the values that exist in this dictionary.
				/// </summary>
				public ICollection<TValue> Values
				{
					get
					{
						using(DisposeLock.ReadLock())
						{
							CheckUndisposed();
							
							var dictionary = _dictionary;

							List<TValue> result = new List<TValue>();
							foreach(GCHandle handle in dictionary.Values)
							{
								TValue item = (TValue)handle.Target;
								if (item != null)
									result.Add(item);
							}
							
							return result;
						}
					}
				}
			#endregion
		#endregion
		#region Methods
			#region _CreateValue
				private static TValue _CreateValue(TKey key, Func<TKey, TValue> createValue)
				{
					if (createValue != null)
						return createValue(key);

					if (_valueConstructor == null)
						throw new InvalidOperationException("Type " + typeof(TValue).FullName + " does not has a public default constructor.");

					return _valueConstructor();
				}
			#endregion

			#region Clear
				/// <summary>
				/// Clears all items in this dictionary.
				/// </summary>
				public void Clear()
				{
					using(DisposeLock.WriteLock())
					{
						CheckUndisposed();
						
						var dictionary = _dictionary;
						try
						{
						}
						finally
						{
							foreach(GCHandle handle in dictionary.Values)
								handle.Free();
							
							dictionary.Clear();
						}
					}
				}
			#endregion
			#region GetOrCreateValue
				/// <summary>
				/// Gets the value for the given key or, if it does not exist, creates it, adds it and
				/// returns it.
				/// If a createValue is not given, the default constructor (if any) is used.
				/// </summary>
				public TValue GetOrCreateValue(TKey key, Func<TKey, TValue> createValue=null)
				{
					if (key == null)
						throw new ArgumentNullException("key");

					bool isHandleGot;
					GCHandle handle;
					int initialCollectionCount = -1;
					using(DisposeLock.ReadLock())
					{
						CheckUndisposed();

						var dictionary = _dictionary;

						isHandleGot = dictionary.TryGetValue(key, out handle);
						if (isHandleGot)
						{
							object result = handle.Target;
							if (result != null)
								return (TValue)result;

							initialCollectionCount = _collectionCount;
						}
					}
					
					using(var upgradeableLock = DisposeLock.UpgradeableLock())
					{
						CheckUndisposed();

						TValue typedResult;
						if (isHandleGot && initialCollectionCount == _collectionCount)
						{
							object result = handle.Target;
							if (result != null)
								return (TValue)result;

							typedResult = _CreateValue(key, createValue);
							upgradeableLock.Upgrade();
							handle.Target = typedResult;
							return typedResult;
						}

						var dictionary = _dictionary;
						if (dictionary.TryGetValue(key, out handle))
						{
							object result = handle.Target;
							if (result != null)
								return (TValue)result;
							
							typedResult = _CreateValue(key, createValue);
							upgradeableLock.Upgrade();
							handle.Target = typedResult;
							return typedResult;
						}

						typedResult = _CreateValue(key, createValue);
						upgradeableLock.Upgrade();
						_Add(key, typedResult, dictionary);
						return typedResult;
					}
				}
			#endregion
			#region Add
				/// <summary>
				/// Adds an item to this dictionary. Throws an exception if an item
				/// with the same key already exists.
				/// </summary>
				public void Add(TKey key, TValue value)
				{
					if (key == null)
						throw new ArgumentNullException("key");
						
					if (value == null)
						throw new ArgumentNullException("value");
						
					using(DisposeLock.WriteLock())
					{
						CheckUndisposed();
						
						var dictionary = _dictionary;
						GCHandle handle;
						if (dictionary.TryGetValue(key, out handle))
						{
							if (handle.Target != null)
								throw new ArgumentException("An element with the same key \"" + key + "\" already exists.");
							
							handle.Target = value;
						}
						else
							_Add(key, value, dictionary);
					}
				}
			#endregion
			#region Remove
				/// <summary>
				/// Removes an item with the given key from the dictionary.
				/// </summary>
				public bool Remove(TKey key)
				{
					if (key == null)
						throw new ArgumentNullException("key");
					
					using(DisposeLock.WriteLock())
					{
						CheckUndisposed();

						return _Remove(key);
					}
				}
			#endregion
			
			#region ContainsKey
				/// <summary>
				/// Gets a value indicating if an item with the specified key exists.
				/// </summary>
				public bool ContainsKey(TKey key)
				{
					return _dictionary.ContainsKey(key);
				}
			#endregion
			
			#region GetEnumerator
				/// <summary>
				/// Gets an enumerator with all key/value pairs that exist in
				/// this dictionary.
				/// </summary>
				public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
				{
					return ToList().GetEnumerator();
				}
			#endregion
			#region ToList
				/// <summary>
				/// Gets a list with all non-collected key/value pairs.
				/// </summary>
				public List<KeyValuePair<TKey, TValue>> ToList()
				{
					using(DisposeLock.ReadLock())
					{
						CheckUndisposed();

						var dictionary = _dictionary;
						var result = new List<KeyValuePair<TKey, TValue>>(dictionary.Count);
						
						foreach(var pair in dictionary)
						{
							TValue target = (TValue)pair.Value.Target;
							if (target != null)
								result.Add(new KeyValuePair<TKey, TValue>(pair.Key, target));
						}

						return result;
					}
				}
			#endregion
			
			#region _Add
				[SuppressMessage("Microsoft.Usage", "CA2219:DoNotRaiseExceptionsInExceptionClauses")]
				private static void _Add(TKey key, TValue value, Dictionary<TKey, GCHandle> dictionary)
				{
					try
					{
					}
					finally
					{
						GCHandle handle = GCHandle.Alloc(value, GCHandleType.Weak);
						try
						{
							dictionary.Add(key, handle);
						}
						catch
						{
							handle.Free();
							throw;
						}
					}
				}
			#endregion
			#region _Remove
				private bool _Remove(TKey key)
				{
					var dictionary = _dictionary;
					GCHandle handle;
					if (!dictionary.TryGetValue(key, out handle))
						return false;

					bool result;
					try
					{
					}
					finally
					{
						handle.Free();
						result = dictionary.Remove(key);
					}
					return result;
				}
			#endregion
		#endregion
		#region Interfaces
			#region IDictionary<TKey,TValue> Members
				bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value)
				{
					value = this[key];
					return value != null;
				}
				TValue IDictionary<TKey, TValue>.this[TKey key]
				{
					get
					{
						TValue result = this[key];
						if (result == null)
							throw new KeyNotFoundException("The given key \"" + key + "\" was not found in the dictionary.");
						
						return result;
					}
					set
					{
						this[key] = value;
					}
				}
			#endregion
			#region ICollection<KeyValuePair<TKey,TValue>> Members
				void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
				{
					Add(item.Key, item.Value);
				}
				bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
				{
					if (item.Value == null)
						return false;
				
					GCHandle handle;
					if (!_dictionary.TryGetValue(item.Key, out handle))
						return false;
					
					return object.Equals(handle.Target, item.Value);
				}
				void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
				{
					ToList().CopyTo(array, arrayIndex);
				}
				bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly
				{
					get
					{
						return false;
					}
				}
				bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
				{
					if (item.Value == null)
						return false;
				
					using(var upgradeableLock = DisposeLock.UpgradeableLock())
					{
						CheckUndisposed();
						
						GCHandle handle;
						var dictionary = _dictionary;
						if (!dictionary.TryGetValue(item.Key, out handle))
							return false;
						
						if (!object.Equals(handle.Target, item.Value))
							return false;

						upgradeableLock.Upgrade();
						return _Remove(item.Key);
					}
				}
			#endregion
			#region IEnumerable Members
				IEnumerator IEnumerable.GetEnumerator()
				{
					return GetEnumerator();
				}
			#endregion

			#region ISerializable Members
				/// <summary>
				/// Creates the dictionary from serialization info.
				/// Actually, it does not load anything, as if everything was
				/// collected.
				/// </summary>
				internal WeakDictionary(SerializationInfo info, StreamingContext context):
					this()
				{
				}
				void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
				{
				}
			#endregion
		#endregion
	}
}
